package com.appspot.codelab.docs.client.widgets;

import java.util.ArrayList;
import java.util.Arrays;

import com.appspot.codelab.docs.client.content.icons.Icons;
import com.appspot.codelab.docs.client.impl.Map;
import com.appspot.codelab.docs.client.repository.RepositoryDirectory;
import com.appspot.codelab.docs.client.repository.RepositoryDirectory.RepositoryFile;
import com.google.gwt.event.logical.shared.OpenEvent;
import com.google.gwt.event.logical.shared.OpenHandler;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RadioButton;
import com.google.gwt.user.client.ui.Tree;
import com.google.gwt.user.client.ui.TreeItem;

/**
 * A tree widget displaying a hierarchical folder view.
 */
public class ExplorerTree extends Tree {

  public enum SelectionType {
	NONE,
	FILE,
	DIRECTORY,
	ALL
  }
  
  public enum SelectionMode {
	SINGLE,
	MULTIPLE
  }
	
  public static interface ExpandHandler {
	
	void getDirectory(String path, AsyncCallback<RepositoryDirectory> callback);
	String getUrl(String itemPath);
  }
	
  /**
   * An ExplorerTree document item.
   */
  public static class ExplorerTreeDocumentItem extends ExplorerTreeItem {

	/**
	 * Constructs an ExplorerTree document item.
	 * 
	 * @param label the item's label
	 * @param value the item's value
	 * @param checkable whether the item is checkable
	 */
	public ExplorerTreeDocumentItem(String label, String value,
	    boolean checkable, boolean uniqueSelection) {
	  this(label, value, null, checkable, uniqueSelection);
	}
	
	/**
	 * Constructs an ExplorerTree document item.
	 * 
	 * @param label the item's label
	 * @param identifier the item's identifier
	 * @param value the item's value
	 * @param href the item's link
	 * @param starred whether the item is starred
	 * @param checkable whether the item is checkable
	 * @param starHandler the star event handler
	 */
	public ExplorerTreeDocumentItem(String label, String value, String href,
	    boolean checkable, boolean uniqueSelection) {
	  super(label, value, href, checkable, uniqueSelection);
	  this.setIcon(Icons.editorIcons.Document());
	}
	
  }
  
  /**
   * An ExplorerTree file item.
   */
  public static class ExplorerTreeFileItem extends ExplorerTreeItem {

	/**
	 * Constructs an ExplorerTree file item.
	 * 
	 * @param label the item's label
	 * @param identifier the item's identifier
	 * @param value the item's value
	 * @param starred whether the item is starred
	 * @param checkable whether the item is checkable
	 * @param starHandler the star event handler
	 */
	public ExplorerTreeFileItem(String label, String value, String href,
		boolean checkable, boolean uniqueSelection) {
	  super(label, value, href, checkable, uniqueSelection);
	  this.setIcon(Icons.editorIcons.File());
	}
	
  }
  
  /**
   * An ExplorerTree loading item.
   */
  public static class ExplorerTreeLoadingItem extends ExplorerTreeItem {

	/**
	 * Constructs an ExplorerTree loading item.
	 */
	public ExplorerTreeLoadingItem() {
	  super("Loading...", "", null, false, false);
	}
	
  }
  
  /**
   * An ExplorerTree root item.
   */
  public static class ExplorerTreeRootItem extends ExplorerTreeFolderItem {

	/**
	 * Constructs an ExplorerTree root item.
	 */
	public ExplorerTreeRootItem(String label) {
	  super(label, "", null, false, false);
	  this.setIcon(Icons.editorIcons.OnlineResource());
	}
	
	@Override
	public String getPath() {
	  return "";
	}
  }
  
  /**
   * An ExplorerTree folder item.
   */
  public static class ExplorerTreeFolderItem extends ExplorerTreeItem {
	
	protected boolean childrenLoaded = false;
	  
	/**
	 * Constructs an ExplorerTree folder item.
	 * 
	 * @param label the item's label
	 * @param value the item's value
	 * @param checkable whether the item is checkable
	 */
	public ExplorerTreeFolderItem(String label, String value,
	    boolean checkable, boolean uniqueSelection) {
	  this(label, value, null, checkable, uniqueSelection);
	}
	
	/**
	 * Constructs an ExplorerTree folder item.
	 * 
	 * @param label the item's label
	 * @param value the item's value
	 * @param href the item's link
	 * @param checkable whether the item is checkable
	 */
	public ExplorerTreeFolderItem(String label, String value, String href,
	    boolean checkable, boolean uniqueSelection) {
	  super(label, value, href, checkable, uniqueSelection);
	  this.setIcon(Icons.editorIcons.Folder());
	}
	
  }
  
  /**
   * The base implementation of an ExplorerTree item.
   */
  public static abstract class ExplorerTreeItem extends TreeItem {
	
	protected CheckBox checkBox;
	protected String label, value;
	protected HorizontalPanel panel;
	
	/**
	 * Constructs an ExplorerTree item.
	 * 
	 * @param label the item's label
	 * @param identifier the item's identifier
	 * @param value the item's value
	 * @param href the item's link
	 * @param starred whether the item is starred
	 * @param checkable whether the item is checkable
	 * @param starHandler the star event handler
	 */
	public ExplorerTreeItem(String label, String value, String href,
		boolean checkable, boolean uniqueSelection) {
	  this.label = label;
      this.value = value;
	  this.panel = new HorizontalPanel();
	  if (checkable) {
		if (uniqueSelection) {
		  this.checkBox = new RadioButton("et", label);
		} else {
		  this.checkBox = new CheckBox(label);
		}
		this.panel.add(this.checkBox);
	  } else {
	    if (href == null) {
		  this.panel.add(new Label(label));
	    } else {
		  Anchor link = new Anchor(label);
          link.setTarget("_blank");
          link.setHref(href);
		  this.panel.add(link);
	    }
	  }
	  setWidget(this.panel);
	}
	
	/**
	 * Retrieves the item's label.
	 * 
	 * @return the item's label
	 */
	public String getLabel() {
	  return this.label;
	}
	
	/**
	 * Retrieves the item's value.
	 * 
	 * @return the item's value
	 */
	public String getValue() {
	  return this.value;
	}
	
	/**
	 * Compiles the path from the root node to this node.
	 * 
	 * @return the path from the root node to this node
	 */
	public String getPath() {
	  if (this.getParentItem() != null &&
		  this.getParentItem() instanceof ExplorerTreeItem) {
		return ((ExplorerTreeItem) this.getParentItem()).getPath() + "/" + getValue();
	  }
	  return "/" + getValue();
	}
	
	/**
	 * Whether the item is selected.
	 */
	public boolean isSelected() {
	  return this.checkBox != null && this.checkBox.getValue();
	}
	
	/**
	 * Sets the item's icon.
	 * 
	 * @param icon the item's icon
	 */
	public void setIcon(ImageResource icon) {
	  if (this.checkBox == null) {
		int i = 0;
		if (this.panel.getWidgetCount() > 1) {
	      this.panel.remove(0);
		}
        this.panel.insert(new Image(icon), i);
	  } else {
	    String html = "<img src=\"" + icon.getURL() + "\" /> " + this.checkBox.getText();
	    this.checkBox.setHTML(html);
	  }
	}
	
  }

  protected SelectionType selectionType = SelectionType.FILE;
  protected SelectionMode selectionMode = SelectionMode.SINGLE;
  protected String rootLabel, expandPath;
  protected ExpandHandler expander;
  
  /**
   * Constructs an ExplorerTree.
   * 
   * @param allowSelection whether to allow selection of files
   * @param showIdentifiers whether to show file identifiers
   * @param starHandler the star event handler
   */
  public ExplorerTree(String rootLabel, final String expandPath,
	    SelectionType selectionType, SelectionMode selectionMode,
	    ExpandHandler expander) {
	super(new Tree.Resources() {
		@Override
		public ImageResource treeOpen() {
		  return Icons.editorIcons.Collapse();
		}
		@Override
		public ImageResource treeLeaf() {
		  return Icons.editorIcons.Blank();
		}
		@Override
		public ImageResource treeClosed() {
		  return Icons.editorIcons.Expand();
		}
	});
	this.selectionType = selectionType;
	this.selectionMode = selectionMode;
	this.expander = expander;
	this.rootLabel = rootLabel;
	this.expandPath = expandPath;
	this.expander.getDirectory(null, new AsyncCallback<RepositoryDirectory>() {
		@Override
		public void onFailure(Throwable caught) {
		}
		@Override
		public void onSuccess(RepositoryDirectory result) {
		  expandItem(null, result);
	      if (expandPath != null) {
		    expandPath(expandPath);
	      }
		}
	});
	this.addOpenHandler(new OpenHandler<TreeItem>() {
		@Override
		public void onOpen(OpenEvent<TreeItem> event) {
		  final ExplorerTreeFolderItem item = (ExplorerTreeFolderItem) event.getTarget();
		  if (item.childrenLoaded) return;
	      ExplorerTree.this.expander.getDirectory(item.getPath(),
	      new AsyncCallback<RepositoryDirectory>() {
		      @Override
		      public void onFailure(Throwable caught) {
		      }
		      @Override
		      public void onSuccess(RepositoryDirectory result) {
		        expandItem(item, result);
		      }
	      });
		}
	});
  }
  
  /**
   * Retrieves the selected entries.
   * 
   * @return the selected entries
   */
  public ArrayList<ExplorerTreeItem> getSelectedEntries() {
	ArrayList<ExplorerTreeItem> selected = new ArrayList<ExplorerTreeItem>();
	for(int i=0; i<this.getItemCount(); i++) {
	  getSelectedChildEntries((ExplorerTreeItem) this.getItem(i), selected);
	}
    return selected;
  }
  
  public void expandPath(String path) {
	final PathIterator autoPath = new PathIterator(path);
	new Object() {
		public void expandNext(int delay) {
		  new Timer() {
		      @Override
			  public void run() {
		    	expandNext();
			  }
		  }.schedule(delay);
		}
		public void expandNext() {
		  final ExplorerTreeFolderItem folder = autoPath.getNextItem();
		  if (folder != null) {
            folder.setState(true);
			expander.getDirectory(autoPath.getCurrentPath(),
			    new AsyncCallback<RepositoryDirectory>() {
				@Override
				public void onFailure(Throwable caught) {
				}
				@Override
				public void onSuccess(RepositoryDirectory result) {
				  expandItem(folder, result);
				  expandNext(100);
				}
			});
		  }
		}
	}.expandNext(1000);
  }
  
  public void refresh() {
	this.removeItems();
	this.expander.getDirectory(null, new AsyncCallback<RepositoryDirectory>() {
		@Override
		public void onFailure(Throwable caught) {
		}
		@Override
		public void onSuccess(RepositoryDirectory result) {
		  expandItem(null, result);
		  if (expandPath != null) {
		    expandPath(expandPath);
	      }
		}
	});
  }
  
  protected ExplorerTreeItem getChildEntry(ExplorerTreeItem item,
      String childName) {
	if (this.getItemCount() == 0) {
	  return null;
	}
	if (item == null) {
	  item = (ExplorerTreeItem) this.getItem(0);
	}
	for (int i=0; i<item.getChildCount(); i++) {
	  ExplorerTreeItem child = (ExplorerTreeItem) item.getChild(i);
	  if (child.getValue().equals(childName)) {
		return child;
	  }
	}
	return null;
  }
  
  protected void getSelectedChildEntries(ExplorerTreeItem item,
      ArrayList<ExplorerTreeItem> selected) {
	for(int i=0; i<item.getChildCount(); i++) {
	  ExplorerTreeItem child = (ExplorerTreeItem) item.getChild(i);
	  if (child.isSelected()) {
		selected.add(child);
	  }
	  getSelectedChildEntries(child, selected);
	}
  }
  
  protected void expandItem(ExplorerTreeFolderItem item, RepositoryDirectory dir) {
	if (item == null) {
	  item = new ExplorerTreeRootItem(rootLabel);
      addItem(item);
	} else {
	  item.removeItems();
	}
	Map<RepositoryDirectory> subdirs = dir.getSubDirs();
    String[] subdirPaths = subdirs.keys();
    Arrays.sort(subdirPaths);
    for (String dirPath : subdirPaths) {
	  RepositoryDirectory subdir =  subdirs.get(dirPath);
	  ExplorerTreeItem subItem = new ExplorerTreeFolderItem(
	      subdir.getName(), subdir.getName(),
	      selectionType == SelectionType.DIRECTORY || selectionType == SelectionType.ALL,
	      selectionMode == SelectionMode.SINGLE);
	  if (subdir.getTotalSubdirs() > 0 ||
		  subdir.getTotalFiles() > 0) {
	    ExplorerTreeItem loadingItem = new ExplorerTreeLoadingItem();
	    subItem.addItem(loadingItem);
	  }
	  item.addItem(subItem);
    }
    if (selectionType != SelectionType.DIRECTORY) {
      Map<RepositoryFile> subfiles = dir.getFilePage().getFiles();
      String[] subfilePaths = subfiles.keys();
      Arrays.sort(subfilePaths);
      for (String filePath : subfilePaths) {
	    RepositoryFile file = subfiles.get(filePath);
	    ExplorerTreeItem subItem = new ExplorerTreeFileItem(
	        file.getName(), file.getName(),
	        expander.getUrl(item.getPath() + "/" + file.getName()),
	        selectionType == SelectionType.FILE || selectionType == SelectionType.ALL,
	        selectionMode == SelectionMode.SINGLE);
	    item.addItem(subItem);
      }
    }
	item.setState(true);
	item.childrenLoaded = true;
  }

  protected class PathIterator {
	
	protected String currentPath;
	protected String[] items;
	protected ExplorerTreeFolderItem currentItem;
	protected int index;
	  
	public PathIterator(String path) {
	  currentPath = "";
	  items = path.split("/");
	  index = 0;
	}
	
	public String getCurrentPath() {
	  return currentPath;
	}
	
	public ExplorerTreeFolderItem getNextItem() {
	  if (index < items.length - 1) {
		String name = items[index++];
		if (name.equals("")) {
		  return getNextItem();
		}
		currentPath += name + "/";
		ExplorerTreeItem item = getChildEntry(currentItem, name);
		if (item != null && item instanceof ExplorerTreeFolderItem) {
	      currentItem = (ExplorerTreeFolderItem) item;
	      return currentItem;
		}
	  }
	  return null;
	}
  }
}
